package com.apobates.jforum2.troll.threads.controller;

import com.apobates.jforum2.threads.stub.TopicCommonResponseVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsAlbumPictureRecordVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsAlbumPictureVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsAlbumVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsCategoryVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsCollectStatsVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsConfigVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsStatsVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsTagRelateVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsTagVo;
import com.apobates.jforum2.threads.stub.vo.ThreadsVo;
import com.apobates.jforum2.troll.attention.ImageDirectoryEnum;
import com.apobates.jforum2.troll.attention.core.UploadImagePathConvert;
import com.apobates.jforum2.troll.regular.ForumActionEnum;
import com.apobates.jforum2.troll.regular.ImageIOMeta;
import com.apobates.jforum2.troll.threads.biz.AlbumPictureService;
import com.apobates.jforum2.troll.threads.biz.AlbumService;
import com.apobates.jforum2.troll.threads.biz.BoardTopicCategoryIndexService;
import com.apobates.jforum2.troll.threads.biz.TopicConfigService;
import com.apobates.jforum2.troll.threads.biz.TopicService;
import com.apobates.jforum2.troll.threads.biz.TopicStatsService;
import com.apobates.jforum2.troll.threads.biz.TopicTagService;
import com.apobates.jforum2.troll.threads.entity.Album;
import com.apobates.jforum2.troll.threads.entity.AlbumPicture;
import com.apobates.jforum2.troll.threads.entity.BoardTopicCategoryIndex;
import com.apobates.jforum2.troll.threads.entity.ForumReportTypeEnum;
import com.apobates.jforum2.troll.threads.entity.TagRelateTopic;
import com.apobates.jforum2.troll.threads.entity.Topic;
import com.apobates.jforum2.troll.threads.entity.TopicCategory;
import com.apobates.jforum2.troll.threads.entity.TopicConfig;
import com.apobates.jforum2.troll.threads.entity.TopicStats;
import com.apobates.jforum2.troll.threads.entity.TopicTag;
import com.apobates.jforum2.troll.threads.mapper.ThreadsConfigVoMapperEntity;
import com.apobates.jforum2.troll.threads.mapper.ThreadsStatsVoMapperEntity;
import com.apobates.jforum2.troll.threads.mapper.ThreadsVoMapperEntity;
import com.apobates.jforum2.troll.threads.plug.FeedbackPlugTopic;
import com.apobates.jforum2.troll.threads.plug.ReportPlugTopic;
import com.apobates.jforum2.troll.utils.core.Commons;
import com.apobates.jforum2.troll.utils.core.DateTimeUtils;
import com.apobates.jforum2.troll.utils.core.EntityPageResult;
import com.apobates.jforum2.troll.utils.core.ResponseResult;
import com.apobates.jforum2.troll.utils.core.StringSeekUtils;
import com.apobates.jforum2.troll.utils.core.api.ActionEventCulpritor;
import com.apobates.jforum2.troll.utils.core.api.AnonyActionEventCulpritor;
import com.apobates.jforum2.troll.utils.persistence.Page;
import com.apobates.jforum2.troll.utils.persistence.PageRequest;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 话题Api
 *
 * @author xiaofanku
 * @since 20201114
 */
@RestController
@RequestMapping("/topic")
public class TopicController {
    @Autowired
    private TopicService topicService;
    @Autowired
    private TopicStatsService topicStatsService;
    @Autowired
    private TopicConfigService topicConfigService;
    @Autowired
    private TopicTagService topicTagService;
    @Autowired
    private AlbumService albumService;
    @Autowired
    private AlbumPictureService albumPictureService;
    @Autowired
    private BoardTopicCategoryIndexService boardTopicCategoryIndexService;
    @Value("${img.bucket.domain}")
    private String imageBucketDomain;
    @Value("${img.bucket.upload.direct}")
    private String imageBucketUploadDirect;
    private final static Logger logger = LoggerFactory.getLogger(TopicController.class);

    //1:查看指定的话题
    @GetMapping("/{id}")
    public ResponseEntity<ThreadsVo> get(@PathVariable("id") long topicId){ //K
        //R:ThreadsVo
        Supplier<Optional<Topic>> topic = ()->(topicId>0)?topicService.get(topicId):Optional.empty();
        ResponseResult<ThreadsVo> rr = ResponseResult.ofEntityVo(topic, new ThreadsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //2:浏览话题
    @PutMapping(value="/{id}/browse", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> browseTopic(@PathVariable("id") long topicId,
                                @RequestParam("member") long memberId,
                                @RequestParam("nickname") String memberNickname,
                                @RequestParam("token") String token,
                                @RequestParam("ip") String ipAddr){
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Supplier<Optional<Boolean>> resultFun = ()->topicService.browse(topicId, aec);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "记录话题浏览失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    /*
    //3:改由Nest实现
    @GetMapping("/{id}/fold/{board}/{volume}")
    public Response getCascade(@PathVariable("id") long topicId, @PathVariable("volume")long boardId, @PathVariable("volume")int boardGroupId){
    }*/
    
    //4:查看指定版块下的指定类型的话题,不包括删除;级联加载话题统计
    @GetMapping("/board/{board}/{category}/{page}/{number}")
    public EntityPageResult<ThreadsCollectStatsVo> getAll(
            @PathVariable("board")long boardId,
            @PathVariable(value="category", required=true)String categoryValue,
            @PathVariable("page")int page,
            @PathVariable("number")int pageSize){
        //加载话题统计{ThreadsCollectStatsVo}
        String cc = "";
        if(Commons.isNotBlank(categoryValue) && !"-".equals(categoryValue)){
            cc = categoryValue;
        }
        //tops:置顶,goods:精华,pic:图片,hots:火贴
        //R:ForumThreads
        Page<Topic> rs = Page.empty();
        PageRequest pr=new PageRequest(page, pageSize);
        if(cc.equalsIgnoreCase("-") || !Arrays.asList("tops", "goods", "hots").contains(cc)) {
            rs = topicService.getAll(boardId, cc, pr);
        }
        if(cc.equalsIgnoreCase("tops")){
            rs = topicService.getAllTopsForBoard(boardId, pr);
        }
        if(cc.equalsIgnoreCase("goods")){
            rs = topicService.getAllGoodsForBoard(boardId, pr);
        }
        if(cc.equalsIgnoreCase("hots")){
            rs = topicService.getAllHotsForBoard(boardId, pr);
        }
        List<ThreadsCollectStatsVo> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList())).collect(Collectors.toList());
        //
        EntityPageResult<ThreadsCollectStatsVo> rr = EntityPageResult.of(rs.getTotalElements(),page,pageSize, data);
        return rr;
    }
    //5:查看指定版块组最近发布的话题,方法将级联加载话题统计
    @GetMapping("/volume/{volume}/recent")
    public List<ThreadsCollectStatsVo> getRecent(@PathVariable("volume") int boardGroupId, @RequestParam("size") int showSize){
        //加载话题统计{ThreadsCollectStatsVo}
        //R:ForumThreads
        Stream<Topic> rs = topicService.getRecent(boardGroupId, showSize);
        return associationTopicStatsAsync(rs.collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    //6:查看指定版块组(卷)下的最多回复的话题
    @GetMapping("/volume/{volume}/maxreply")
    public List<ThreadsVo> getMaxReplyForBoardGroup(@PathVariable("volume") int boardGroupId, @RequestParam("size")int showSize){
        //R:ForumThreadsDigest
        Stream<Topic> rs = topicService.getMaxReplyForBoardGroup(boardGroupId, showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //7:查看指定版块组(卷)下的精华话题
    @GetMapping("/volume/{volume}/good")
    public List<ThreadsVo> getGoodsForBoardGroup(@PathVariable("volume") int boardGroupId, @RequestParam("size") int showSize){
        //R:ForumThreadsDigest
        Stream<Topic> rs = topicService.getGoodsForBoardGroup(boardGroupId, showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //8:查看回复最多的话题
    @GetMapping("/stats/maxreply")
    public List<ThreadsVo> getHot(@RequestParam("size")int showSize){
        //R:ForumThreadsDigest
        Stream<Topic> rs = topicService.getHot(showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //9:指定版块置顶的话题,不包括删除;会级联加载话题统计,最多显示5条
    @GetMapping("/board/{board}/top")
    public List<ThreadsCollectStatsVo> getTopForBoard(@PathVariable("board") long boardId){
        //加载话题统计{ThreadsCollectStatsVo}
        //R:ForumThreads
        Stream<Topic> rs = topicService.getTopForBoard(boardId);
        return associationTopicStatsAsync(rs.collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    //10:自参考日期以后版块最近的话题,会级联加载话题统计
    @GetMapping("/board/{board}/recent/{stamp}")
    public List<ThreadsCollectStatsVo> getRecentByUnixStamp(@PathVariable("board")long boardId, @PathVariable("stamp")int previousUx){
        //加载话题统计{ThreadsCollectStatsVo}
        //R:ForumThreads
        Stream<Topic> rs = topicService.getRecentByUnixStamp(boardId, previousUx);
        return associationTopicStatsAsync(rs.collect(Collectors.toList())).collect(Collectors.toList());
    }
    //11:自参考日期以后版块最近的话题数量
    @GetMapping("/board/{board}/recent/{stamp}/size")
    public long getRecentByUnixStampSize(@PathVariable("board")long boardId, @PathVariable("stamp")int previousUx){
        long size = topicService.getRecentByUnixStampSize(boardId, previousUx);
        return size; //ResponseResult.size(size).toJson();
    }
    //12:查看最近发布的话题,不包括删除的,不包含举报和反馈类型的.方法会级联加载话题统计({版块}改由Nest补正)
    @GetMapping("/home/recent")
    public List<ThreadsCollectStatsVo> getRecentRelateContent(@RequestParam("size") int showSize){
        //加载话题统计 & 版块
        Stream<Topic> rs = topicService.getRecentRelateContent(showSize);
        return associationTopicStatsAsync(rs.collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    //13:查看指定的话题,加载话题的统计信息({会员信息|话题内容}改由Nest补正)
    @GetMapping("/{id}/cascade/stats")
    public ResponseEntity<ThreadsCollectStatsVo> getCaseStats(@PathVariable("id")long topicId){
        CompletableFuture<Optional<Topic>> r1 = CompletableFuture.supplyAsync(()->topicService.get(topicId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        CompletableFuture<Optional<TopicStats>> r2 = CompletableFuture.supplyAsync(()->topicStatsService.getByTopic(topicId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        ThreadsCollectStatsVo data = r1.thenCombine(r2, (tOpt, tsOpt)->{
            if(!tOpt.isPresent()){
                return null;
            }
            Topic t = tOpt.get();
            ThreadsCollectStatsVo tmp = new ThreadsCollectStatsVo();
            tmp.setThreadsVo(ThreadsVoMapperEntity.mapper().apply(t));
            tmp.setStatsVo(ThreadsStatsVoMapperEntity.mapper().apply(tsOpt.orElse(TopicStats.empty(topicId, t.getMemberId(), t.getMemberNickname()))));
            return tmp;
        }).orTimeout(1, TimeUnit.SECONDS).join();
        //
        ResponseResult<ThreadsCollectStatsVo> rr = ResponseResult.Helper.ofEntity(data);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //查看指定的话题,加载话题作者的个人信息
    //ETC

    /*14:改由Nest实现
    @GetMapping("/{id}/content/{scale}")
    public Response getDecodeContent(@PathVariable("id")long topicId, @DefaultValue("auto")@PathVariable(value="scale", required=false, defaultValue="auto") String imageScale){
    }*/
    //15:查看指定会员发布的所有话题,会级联加载话题统计
    @GetMapping("/member/recent/{member}/{page}/{number}")
    public EntityPageResult<ThreadsCollectStatsVo> getAllForMember(@PathVariable("member") long memberId, @PathVariable("page") int page, @PathVariable("number") int pageSize){
        //加载话题统计{ThreadsCollectStatsVo}
        //R:ForumActionRecord
        Page<Topic> rs = topicService.getAllForMember(memberId, new PageRequest(page, pageSize));
        List<ThreadsCollectStatsVo> data = associationTopicStatsAsync(rs.getResult().collect(Collectors.toList())).collect(Collectors.toList());
        //
        EntityPageResult<ThreadsCollectStatsVo> rr = EntityPageResult.of(rs.getTotalElements(),page,pageSize, data);
        return rr;
    }
    /*15.1:查看会员回复的话题,话题的作者不是该会员(不包含自已发的),会级联加载话题的统计
    @GetMapping("/member/reply/{member}/{page}/{number}")
    public EntityPageResult<ThreadsCollectStatsVo> getAllForMemberReply(@PathVariable("member") long memberId, @PathVariable("page") int page, @PathVariable("number") int pageSize){
        //加载话题统计{ThreadsCollectStatsVo}
        //R:ForumActionRecord
        Page<TopicReplica> rs = topicService.getAllForMemberReply(memberId, new PageRequest(page, pageSize));
        return ResponseResult.PageResultBuilder.<ThreadsCollectStatsVo,TopicReplica>getInstance(page, pageSize)
                .from(rs.getTotalElements(), rs.getResult())
                .build(ThreadsReplicaMapper.threadsCollectStats())
                .toJson();
    }*/
    //16:查看指定会员最近回复的话题,话题的作者不是该会员(不包含自已发的),级联加载版块
    @GetMapping("/member/reply/{member}")
    public List<ThreadsVo> getAllForMemberReply(@PathVariable("member") long memberId, @RequestParam("size") int showSize){
        //级联加载版块
        //R:CommonTopic
        //Stream<TopicReplica> rs = topicService.getAllForMemberReply(memberId, showSize).sorted(Comparator.comparing(Topic::getRankingDateTime).reversed());
        //return rs.map(ThreadsReplicaMapper.threadsCollectBoard()).collect(Collectors.toList());
        Stream<Topic> rs = topicService.getAllForMemberReply(memberId, showSize).sorted(Comparator.comparing(Topic::getRankingDateTime).reversed());
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //17:查看指定会员最近发布的话题({版块}改由Nest补正)
    @GetMapping("/member/recent/{member}")
    public List<ThreadsVo> getRecentForMember(@PathVariable("member") long memberId, @RequestParam("size") int showSize){
        //showSize
        //R:CommonTopic
        Stream<Topic> rs = topicService.getRecentForMember(memberId, showSize).sorted(Comparator.comparing(Topic::getEntryDateTime).reversed());
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    
    //18:查看会员发布的受欢迎的话题列表级联加载话题统计({版块}改由Nest补正)
    @GetMapping("/member/popular/{member}")
    public List<ThreadsCollectStatsVo> getAllForMemberPopular(@PathVariable("member") long memberId, @RequestParam("size") int showSize){
        Stream<Topic> rs = topicService.getAllForMemberPopular(memberId, showSize);
        return associationTopicStatsAsync(rs.collect(Collectors.toList())).collect(Collectors.toList());
    }
    
    //19:查看指定版块最近的话题,不包括删除,置顶;包括加精,普通;以rankingDateTime倒序 服务于RSS
    @GetMapping("/rss/recent/board/{board}")
    public List<ThreadsVo> getRecentForBoard(@PathVariable("board") long boardId, @RequestParam("size") int showSize){
        //R:ThreadsVo
        Stream<Topic> rs = topicService.getRecentForBoard(boardId, showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //20:查看最近发布的话题,不包括删除的,不包含1号(举报和反馈所在)版块的
    //RSS
    @GetMapping("/rss/recent")
    public List<ThreadsVo> getRecent(@RequestParam("size") int showSize){
        //R:ThreadsVo
        Stream<Topic> rs = topicService.getRecent(showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //21:根据话题查看话题配置
    @GetMapping("/{id}/config")
    public ResponseEntity<ThreadsConfigVo> getThreadsConfig(@PathVariable("id") long topicId){
        Supplier<Optional<TopicConfig>> rs = ()->topicConfigService.getByTopicId(topicId);
        ResponseResult<ThreadsConfigVo> rr = ResponseResult.ofEntityVo(rs, new ThreadsConfigVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }

    //22:查看话题的统计
    @GetMapping("/{id}/stats")
    public ResponseEntity<ThreadsStatsVo> getThreadsStats(@PathVariable("id") long topicId){
        //R:TopicStats
        Supplier<Optional<TopicStats>> rs = ()->topicStatsService.getByTopic(topicId);
        ResponseResult<ThreadsStatsVo> rr = ResponseResult.ofEntityVo(rs, new ThreadsStatsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //23查看指定话题的封面
    @GetMapping("/{id}/cover/{scale}")
    public ResponseEntity<String> getThreadsCover(@PathVariable("id") long topicId, @PathVariable("scale") String scale){
        Optional<String> imagePath = Optional.empty(); //"/static/img/default.poster.bg.jpg";
        try{
            Album album = albumService.getByTopic(topicId).orElse(null);
            //不希望添加水印可以使用ForumEntityIcoDecorder解码封面地址
            imagePath = new UploadImagePathConvert(getImageIOMeta().getImageBucketDomain(), getImageIOMeta().getUploadImageDirectName())
                    .decode(album.getCoverLink(), ImageDirectoryEnum.DEFAT, scale)
                    .map(URL::toString); //使用默认的封面图
        }catch(NullPointerException | IOException e){
            if(logger.isDebugEnabled()){
                logger.debug("[Args]topic#"+topicId+" has error message:"+e.getMessage());
            }
        }
        ResponseResult<String> rr = ResponseResult.Helper.ofOptional(imagePath);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    
    //24:是否可以收藏, 可以返回true
    @GetMapping("/{id}/favorite/member/{member}")
    public boolean isFavoriteTopic(@PathVariable("id") long topicId, @PathVariable("member") long memberId){
        return topicService.isFavorited(topicId, memberId);
    }
    //25:是否可以点赞, 可以返回true
    @GetMapping("/{id}/like/member/{member}")
    public boolean isLikeTopic(@PathVariable("id") long topicId, @PathVariable("member") long memberId){
        return topicService.isLiked(topicId, memberId);
    }
    //26:联合检查:
    // 1)是否可以收藏, 可以返回true
    // 2)是否可以点赞, 可以返回true
    /*Angular采用的
    @GetMapping("/{id}/check/{member}/operate")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public void getThreadsUnionResult(@Context SseEventSink eventSink, @Context Sse sse, @PathVariable("id") long topicId, @PathVariable("member") long memberId) {
        ThreadsUnionCheckVo rs = new ThreadsUnionCheckVo();
        rs.setId(topicId);
        rs.setFavorite(topicService.isFavorited(topicId, memberId)?"1":"0");
        rs.setLike(topicService.isLiked(topicId, memberId)?"1":"0");
        //
        final OutboundSseEvent event = sse.newEventBuilder()
                .name("ThreadsUnionCheck")
                .mediaType(MediaType.APPLICATION_JSON_TYPE)
                .data(ThreadsUnionCheckVo.class, rs)
                .build();
        eventSink.send(event);
        //CompletableFuture.runAsync(() -> eventSink.send(event)).whenComplete((r, ex) -> eventSink.close());
    }*/
    /*27:55*/
    //28:反馈
    @PostMapping(value="/feedback", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<ThreadsVo> feedback(@RequestParam("title") String title,
                             @RequestParam("content") String content,
                             @RequestParam("member") long memberId,
                             @RequestParam("nickname") String memberNickname,
                             @RequestParam("token") String token,
                             @RequestParam("ip") String ipAddr){
        Optional<BoardTopicCategoryIndex> btc = boardTopicCategoryIndexService.getFeedbackRelativeRecord();
        if(!btc.isPresent()){
            return ResponseEntity.badRequest().body(new ThreadsVo()); //("未知的存储目标");
        }
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Supplier<Optional<Topic>> ins = ()->topicService.plug(new FeedbackPlugTopic(aec, title, content, btc.get()));
        ResponseResult<ThreadsVo> rr = ResponseResult.ofEntityVo(ins, new ThreadsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //29:移动话题
    @PutMapping(value="/{id}/move", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> move(@PathVariable("id") long topicId,
                         @RequestParam("board") long srcBoardId,
                         @RequestParam("target") long targetBoardId,
                         @RequestParam("targetBoardGroup") int targetBoardGroupId,
                         @RequestParam("member") long memberId,
                         @RequestParam("nickname") String memberNickname,
                         @RequestParam("token") String token,
                         @RequestParam("ip") String ipAddr){
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Supplier<Optional<Boolean>> resultFun = ()->topicService.move(topicId, srcBoardId, targetBoardId, targetBoardGroupId, aec);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "话题移动失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //30:创建话题
    @PostMapping(value="/create", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<ThreadsVo> createThreads(@RequestParam("volume") int volumesId,
                                  @RequestParam("board") long boardId,
                                  @RequestParam("category") int category,
                                  @RequestParam("title") String title,
                                  @RequestParam("content") String content,
                                  @RequestParam("member") long memberId,
                                  @RequestParam("nickname") String memberNickname,
                                  @RequestParam("anony") String isAnonyStr,
                                  @RequestParam("token") String token,
                                  @RequestParam("ip") String ipAddr){
        logger.error("[threads-api]CTL start publish topic");
        AnonyActionEventCulpritor aec = getAnonyInstance(memberId, memberNickname, isAnonyStr, ipAddr, token);
        Supplier<Optional<Topic>> resultFun = ()->topicService.create(volumesId, boardId, category, title, content, getImageIOMeta(), aec);
        ResponseResult<ThreadsVo> rr = ResponseResult.ofEntityVo(resultFun, new ThreadsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }

    /**
     * 31:查看指定版块最近的话题, 忽略状态;以entryDateTime倒序 服务于策略检查
     * @param boardId 版块ID
     * @param showSize 显示的数量
     * @return
     */
    @GetMapping("/board/{board}/recent")
    public List<ThreadsVo> getRecentForBoardIgnoreStatus(@PathVariable("board") long boardId, @RequestParam("size") int showSize){
        Stream<Topic> rs = topicService.getRecentForBoardIgnoreStatus(boardId, showSize);
        return rs.map(ThreadsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    
    //32:查看版块话题可用的话题类型列表. 注意版块必须与版块组(卷)是包含关系
    @GetMapping("/category/{board}/{volume}")
    public List<ThreadsCategoryVo> getAllByBoardTopicCategories(@PathVariable("board")long boardId, @PathVariable("volume") int boardGroupId){
        Stream<TopicCategory> rs = boardTopicCategoryIndexService.getAllByBoardTopicCategories(boardGroupId, boardId);
        return rs.map(categoryMapper()).collect(Collectors.toList());
    }

    //33:置顶话题
    @PutMapping(value="/{id}/top", consumes="application/x-www-form-urlencoded")
    public boolean topTopic(@PathVariable("id") long topicId,
                             @RequestParam("member") long memberId,
                             @RequestParam("nickname") String memberNickname,
                             @RequestParam("token") String token,
                             @RequestParam("ip") String ipAddr){
        Optional<Boolean> actionFunction = topicService.editTop(topicId, getInstance(memberId, memberNickname, ipAddr, token));
        return actionFunction.orElse(false);
    }
    //34:精华话题
    @PutMapping(value="/{id}/good", consumes="application/x-www-form-urlencoded")
    public boolean goodTopic(@PathVariable("id") long topicId,
                              @RequestParam("member") long memberId,
                              @RequestParam("nickname") String memberNickname,
                              @RequestParam("token") String token,
                              @RequestParam("ip") String ipAddr){
        Optional<Boolean> actionFunction = topicService.editGoods(topicId, getInstance(memberId, memberNickname, ipAddr, token));
        return actionFunction.orElse(false);
    }
    //35:锁定话题
    @PutMapping(value="/{id}/lock", consumes="application/x-www-form-urlencoded")
    public boolean lockTopic(@PathVariable("id") long topicId,
                              @RequestParam("member") long memberId,
                              @RequestParam("nickname") String memberNickname,
                              @RequestParam("token") String token,
                              @RequestParam("ip") String ipAddr){
        Optional<Boolean> actionFunction = topicService.lock(topicId, getInstance(memberId, memberNickname, ipAddr, token));
        return actionFunction.orElse(false);
    }
    //36:取消锁定
    @PutMapping(value="/{id}/lock/cancel", consumes="application/x-www-form-urlencoded")
    public boolean unLockTopic(@PathVariable("id") long topicId,
                                @RequestParam("member") long memberId,
                                @RequestParam("nickname") String memberNickname,
                                @RequestParam("token") String token,
                                @RequestParam("ip") String ipAddr){
        Optional<Boolean> actionFunction = topicService.releaseLock(topicId, getInstance(memberId, memberNickname, ipAddr, token));
        return actionFunction.orElse(false);
    }
    //37:删除话题
    @PutMapping(value="/{id}/remove", consumes="application/x-www-form-urlencoded")
    public boolean delTopic(@PathVariable("id") long topicId,
                             @RequestParam("member") long memberId,
                             @RequestParam("nickname") String memberNickname,
                             @RequestParam("token") String token,
                             @RequestParam("ip") String ipAddr){
        Optional<Boolean> actionFunction = topicService.remove(topicId, getInstance(memberId, memberNickname, ipAddr, token));
        return actionFunction.orElse(false);
    }
    //38:收藏话题
    @PutMapping(value="/{id}/favorite", consumes="application/x-www-form-urlencoded")
    public boolean favoriteTopic(@PathVariable("id") long topicId,
                                  @RequestParam("member") long memberId,
                                  @RequestParam("nickname") String memberNickname,
                                  @RequestParam("token") String token,
                                  @RequestParam("ip") String ipAddr){
        logger.error("[threads-api]CTL start favorite topic");
        try {
            Optional<Boolean> actionFunction = topicService.favorite(topicId, getInstance(memberId, memberNickname, ipAddr, token));
            return actionFunction.orElse(false);
        }catch (IllegalStateException e){
            logger.error("[threads-api]favorite fail, exception:"+e.getMessage());
        }
        return false;
    }
    //39:取消收藏
    @PutMapping(value="/{id}/favorite/cancel", consumes="application/x-www-form-urlencoded")
    public boolean cancelFavoriteTopic(@PathVariable("id") long topicId,
                                        @RequestParam("member") long memberId,
                                        @RequestParam("nickname") String memberNickname,
                                        @RequestParam("token") String token,
                                        @RequestParam("ip") String ipAddr){
        logger.error("[threads-api]CTL start cancel favorite topic");
        try {
            Optional<Boolean> actionFunction = topicService.removeFavorite(topicId, getInstance(memberId, memberNickname, ipAddr, token));
            return actionFunction.orElse(false);
        }catch (IllegalStateException  e){
            logger.error("[threads-api]cancel favorite fail, exception:"+e.getMessage());
        }
        return false;
    }
    //40:话题点赞
    @PutMapping(value="/{id}/like", consumes="application/x-www-form-urlencoded")
    public boolean likeTopic(@PathVariable("id") long topicId,
                              @RequestParam("member") long memberId,
                              @RequestParam("nickname") String memberNickname,
                              @RequestParam("token") String token,
                              @RequestParam("ip") String ipAddr){
        logger.error("[threads-api]CTL start like topic");
        try {
            Optional<Boolean> actionFunction = topicService.like(topicId, getInstance(memberId, memberNickname, ipAddr, token));
            return actionFunction.orElse(false);
        }catch (IllegalStateException e){
            logger.error("[threads-api]like fail, exception:"+e.getMessage());
        }
        return false;
    }
    //41:取消话题点赞
    @PutMapping(value="/{id}/like/cancel", consumes="application/x-www-form-urlencoded")
    public boolean unLikeTopic(@PathVariable("id") long topicId,
                    @RequestParam("member") long memberId,
                    @RequestParam("nickname") String memberNickname,
                    @RequestParam("token") String token,
                    @RequestParam("ip") String ipAddr){
        logger.error("[threads-api]CTL start cancel like topic");
        try {
            Optional<Boolean> actionFunction = topicService.removeLike(topicId, getInstance(memberId, memberNickname, ipAddr, token));
            return actionFunction.orElse(false);
        }catch (IllegalStateException e){
            logger.error("[threads-api]cancel like fail, exception:"+e.getMessage());
        }
        return false;
    }
    //42:查看指定像册的图片
    @PostMapping(value="/album/picture/multi", consumes="application/x-www-form-urlencoded")
    public List<ThreadsAlbumPictureVo> getMultiAlbumPicture(@RequestParam("records") String albumIdString,
                                    @RequestParam("scale") String scale,
                                    @RequestParam("default_value") String defaultValue){
        Set<Long> albumIdSet = Commons.toLongSet(albumIdString);
        if (null == albumIdSet || albumIdSet.isEmpty()) {
            return Collections.emptyList();
        }
        Stream<AlbumPicture> rs = albumPictureService.getAll(albumIdSet, getImageIOMeta(), scale, defaultValue);//"/static/img/140x140.png");
        return rs.map(albumPictureMapper()).collect(Collectors.toList());
    }

    //43:查看指定话题的像册
    @PostMapping(value="/album/multi", consumes="application/x-www-form-urlencoded")
    public List<ThreadsAlbumVo> getMultiThreadsCover(@RequestParam("records") String topicIdString,
                                @RequestParam("scale") String scale,
                                @RequestParam("default_value") String defaultValue){
        Set<Long> topicIdSet = Commons.toLongSet(topicIdString);
        if (null == topicIdSet || topicIdSet.isEmpty()) {
            return Collections.emptyList();
        }
        Stream<Album> rs = albumService.getAll(topicIdSet, getImageIOMeta(), scale, defaultValue);//"/static/img/140x140.png");
        return rs.map(albumMapper()).collect(Collectors.toList());
    }
    //44:查看指定的像册
    @GetMapping("/album/{album}")
    public ResponseEntity<ThreadsAlbumVo> getThreadsAlbum(@PathVariable("album")long albumId){
        Optional<Album> rs = albumService.get(albumId);
        ResponseResult<ThreadsAlbumVo> rr = ResponseResult.Helper.ofOptional(rs.map(albumMapper()));
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //45:查看指定像册下的图片
    @PostMapping(value="/album/{album}/pictures", consumes="application/x-www-form-urlencoded")
    public List<ThreadsAlbumPictureRecordVo> getThreadsAlbumPictures(
            @PathVariable("album")long albumId,
            @RequestParam("scale") String scale,
            @RequestParam("default_value") String defaultValue,
            @RequestParam("size") int showSize){
        try {
            Stream<AlbumPicture> rs = albumPictureService.getAll(albumId, getImageIOMeta(), scale, defaultValue, showSize);
            return rs.map(albumPictureRecordMapper()).collect(Collectors.toList());
        }catch (Exception e){
            if(logger.isDebugEnabled()){
                logger.debug(e.getMessage(), e);
            }
        }
        return Collections.emptyList();
    }
    
    //46:查看举报的类别
    @GetMapping("/report/category")
    public List<TopicCommonResponseVo> getReportType(){
        Function<java.util.Map.Entry<Integer,String>, TopicCommonResponseVo> mapper = entry->{
            TopicCommonResponseVo vo = new TopicCommonResponseVo();
            vo.setId(entry.getKey()+"");
            vo.setValue(entry.getValue());
            return vo;
        };
        return ForumReportTypeEnum.getData().entrySet().stream().map(mapper).collect(Collectors.toList());
    }
    //47:根据标签查看相关的话题
    @GetMapping("/search/{word}/{page}/{number}")
    public EntityPageResult<ThreadsVo> getAllForTag(@PathVariable("word")String searchWord, @PathVariable("page")int page, @PathVariable("number")int pageSize){
        List<String> sw = Optional.ofNullable(searchWord).map(str->Arrays.asList(str.split(" "))).orElse(Collections.emptyList());
        Page<Topic> rs = topicService.getAllForTag(sw, new PageRequest(page, pageSize));
        //
        ResponseResult<EntityPageResult<ThreadsVo>> rr = ResponseResult.PageResultBuilder.<ThreadsVo,Topic>getInstance(page, pageSize)
                .from(rs.getTotalElements(), rs.getResult())
                .build(ThreadsVoMapperEntity.mapper());
        return rr.getData();
    }
    //48:查看指定话题相关的话题
    @GetMapping("/{id}/relate")
    public List<ThreadsTagRelateVo> getRelateTopic(@PathVariable("id") long topicId, @RequestParam("size") int showSize){
        Stream<TagRelateTopic> rs = topicService.getRelateTopic(topicId, showSize).sorted();
        return rs.map(trt->{
            ThreadsTagRelateVo ttr = new ThreadsTagRelateVo();
            ttr.setThreads(trt.getTopic().getId());
            ttr.setTitle(trt.getTopic().getTitle());
            ttr.setLink(ThreadsVoMapperEntity.produceLink.apply(trt.getTopic().getId(), trt.getTopic().getBoardId(), trt.getTopic().getVolumesId()));
            ttr.setDate(DateTimeUtils.formatClock(trt.getTopic().getEntryDateTime()));
            ttr.setRanking(trt.getRanking());
            ttr.setSimilarity(trt.getSimilarity());
            return ttr;
        }).collect(Collectors.toList());
    }
    //49:查看指定话题的标签
    @GetMapping("/{id}/tag")
    public List<ThreadsTagVo> getThreadsTag(@PathVariable("id")long topicId){
        List<TopicTag> rs = topicTagService.getAllBy(topicId);
        return rs.stream().map(tt->{
            ThreadsTagVo ttv = new ThreadsTagVo();
            ttv.setId(tt.getId());
            ttv.setWord(tt.getNames());
            ttv.setRates(tt.getRates());
            ttv.setThreads(topicId);
            return ttv;
        }).collect(Collectors.toList());
    }
    //50:创建话题标签
    @PostMapping(value="/{id}/tag/create", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<ThreadsTagVo> createThreadsTag(@PathVariable("id")long topicId,
                                 @RequestParam("tag")String tagNames,
                                 @RequestParam("segment")String tagSegment,
                                 @RequestParam("member") long memberId,
                                 @RequestParam("nickname") String memberNickname,
                                 @RequestParam("token") String token,
                                 @RequestParam("ip") String ipAddr){
        if(!Commons.isNotBlank(tagNames) || !Commons.isNotBlank(tagSegment)){
            ResponseEntity.status(400).contentType(MediaType.APPLICATION_JSON).body(ThreadsTagVo.empty(tagNames, topicId));
        }
        //过滤一下
        String newTagNames = Commons.htmlPurifier(tagNames).trim();
        int rates = StringSeekUtils.queryWordCountByBM(tagSegment, newTagNames);
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        long id = topicTagService.create(topicId, newTagNames, rates, aec);
        //
        Optional<ThreadsTagVo> r = Optional.empty();
        if(id>0){
            ThreadsTagVo ttv = new ThreadsTagVo();
            ttv.setId(id);
            ttv.setWord(newTagNames);
            ttv.setRates(rates);
            ttv.setThreads(topicId);
            r = Optional.of(ttv);
        }
        //
        ResponseResult<ThreadsTagVo> rr = ResponseResult.Helper.ofOptional(r);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //51:从话题中删除指定标签
    @PostMapping(value="/tag/{id}/remove", consumes="application/x-www-form-urlencoded")
    public boolean removeThreadsTag(@PathVariable("id")long tagId,
                       @RequestParam("threads")long topicId,
                       @RequestParam("member") long memberId,
                       @RequestParam("nickname") String memberNickname,
                       @RequestParam("token") String token,
                       @RequestParam("ip") String ipAddr){
        Optional<Boolean> r = topicTagService.deleteForTopic(topicId, tagId, getInstance(memberId, memberNickname, ipAddr, token));
        return r.orElse(false);
    }
    //52:分组统计出现频率最高的标签名称(TopicTag.names)
    @GetMapping("/tag/hot")
    public List<TopicCommonResponseVo> groupTagForNames(@RequestParam("size")int showSize){
        Map<String, Long> rs = topicTagService.groupTagForNames(showSize);
        return rs.entrySet().stream().map(entry->{
            TopicCommonResponseVo crv = new TopicCommonResponseVo();
            crv.setValue(entry.getKey());
            crv.setId(entry.getValue()+"");
            return crv;
        }).collect(Collectors.toList());
    }

    /*53:查看指定版块最近关联的轮播图
    @GetMapping("/carousel/{volumes}/{board}")
    public List<ThreadsCarouselSlideItemVo> getCarouselSlide(@PathVariable("volumes") int boardGroupId, @PathVariable("board")long boardId){
        Optional<TopicCarouselReplica> data = topicCarouselService.get(boardGroupId, boardId, getImageIOMeta());
        return data.map(tcr->{
            return tcr.getSlides().stream().map(new ThreadsCarouselSlideItemVoMapperEntity()).collect(Collectors.toList());
        }).orElse(Collections.emptyList());
    }
    //54:查看指定轮播图的幻灯片,要求幻灯片的状态可用并解码幻灯片的图片地址
    @GetMapping("/carousel/{title}")
    public List<ThreadsCarouselSlideItemVo> getCarouselSlideByTitle(@PathVariable("title")String carouselTitle){
        return topicCarouselSlideService.getAll(carouselTitle, getImageIOMeta()).map(new ThreadsCarouselSlideItemVoMapperEntity()).collect(Collectors.toList());
    }*/

    //55:举报话题/回复
    @PostMapping(value="/{id}/report", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<ThreadsVo> reportTopic(@PathVariable("id") long topicId,
                              @RequestParam("reason") String reportReason,
                              @RequestParam("type") int reportType,
                              @RequestParam("destFloor")long reportFloor,
                              @RequestParam("destNickname")String reportNickname,
                              @RequestParam("destLink")String reportLink,
                              @RequestParam("destContent")String reportSourceContent,
                              @RequestParam("member") long memberId,
                              @RequestParam("nickname") String memberNickname,
                              @RequestParam("token") String token,
                              @RequestParam("ip") String ipAddr){
        Topic topic = topicService.get(topicId).orElse(null);
        if(null == topic){
            return ResponseEntity.badRequest().body(new ThreadsVo()); //("举报的目标不存在");
        }
        Optional<BoardTopicCategoryIndex> btc = boardTopicCategoryIndexService.getReportRelativeRecord();
        if(!btc.isPresent()){
            return ResponseEntity.badRequest().body(new ThreadsVo()); //("未知的存储目标");
        }
        Supplier<Optional<Topic>> ins = ()->topicService.plug(new ReportPlugTopic(
                ForumActionEnum.POSTS_REPORT,
                getInstance(memberId, memberNickname, ipAddr, token),
                topic,
                reportFloor,
                reportNickname,
                reportLink,
                reportSourceContent,
                reportType,
                reportReason,
                btc.get()));
        //
        ResponseResult<ThreadsVo> rr = ResponseResult.ofEntityVo(ins, new ThreadsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }

    //56:从回复中分离出来的事务|更新话题统计的最近回复@20211006
    @PutMapping(value="/{id}/stats/update", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> updateStatsRecentReply(
            @PathVariable("id") long topicId,
            @RequestParam("posts")long recentPostsId,
            @RequestParam("member")long recentPostsMemberId,
            @RequestParam("nickname")String recentPostsMemberNickname){
        Supplier<Optional<Boolean>> resultFun = ()->topicStatsService.updateRecentReply(topicId, recentPostsId, recentPostsMemberId, recentPostsMemberNickname);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "更新话题统计的最近回复失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }

    //[异步]级联话题统计
    private Stream<ThreadsCollectStatsVo> associationTopicStatsAsync(final List<Topic> topices) {
        if (null == topices || topices.isEmpty()) {
            return Stream.empty();
        }
        BiFunction<Topic, TopicStats, ThreadsCollectStatsVo> bi = (topic, topicStats)->{
            ThreadsCollectStatsVo ins = new ThreadsCollectStatsVo();
            //
            ins.setThreadsVo(ThreadsVoMapperEntity.mapper().apply(topic));
            ins.setStatsVo(ThreadsStatsVoMapperEntity.mapper().apply(topicStats));
            return ins;
        };
        Map<Long, TopicStats> topicStatsMap = CompletableFuture.supplyAsync(()->topices.stream().map(Topic::getId).collect(Collectors.toSet()))
                                .thenCompose(topicIdSet-> CompletableFuture.supplyAsync(()->topicStatsService.getByTopicIdList(topicIdSet).collect(Collectors.toMap(TopicStats::getTopicId, Function.identity())))
                                                        .completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS))
                                .orTimeout(1, TimeUnit.SECONDS)
                                .join();
        Comparator<Topic> comparator = (topic, otherTopic) -> topic.getRankingDateTime().compareTo(otherTopic.getRankingDateTime());
        return topices.parallelStream().sorted(comparator.reversed()).map(topic->bi.apply(topic, topicStatsMap.get(topic.getId())));
    }
    public static Function<AlbumPicture, ThreadsAlbumPictureRecordVo> albumPictureRecordMapper() {
        return albumPicture -> {
            ThreadsAlbumPictureRecordVo tapr = new ThreadsAlbumPictureRecordVo();
            tapr.setId(albumPicture.getId());
            //图片的连接地址
            tapr.setLink(albumPicture.getLink());
            //图片的alt内容
            tapr.setCaption(albumPicture.getCaption());
            //状态,false(0)禁用,true(1)可用
            tapr.setStatus(albumPicture.isStatus());
            //是否是封面,false(0)不是,true(1)是
            tapr.setCover(albumPicture.isCover());
            //显示的顺序
            tapr.setRanking(albumPicture.getRanking());
            //缩略图
            tapr.setThumb(albumPicture.getThumb());
            //像册ID
            tapr.setAlbum(albumPicture.getAlbumId());
            return tapr;
        };
    }
    public static Function<TopicCategory,ThreadsCategoryVo> categoryMapper(){
        return topicCategory -> {
            ThreadsCategoryVo tcv = new ThreadsCategoryVo();
            tcv.setId(topicCategory.getId());
            tcv.setValue(topicCategory.getValue());
            tcv.setNames(topicCategory.getNames());
            tcv.setStatus(topicCategory.isStatus());
            return tcv;
        };
    }
    public static Function<AlbumPicture, ThreadsAlbumPictureVo> albumPictureMapper(){
        return albumPicture -> {
            ThreadsAlbumPictureVo tap = new ThreadsAlbumPictureVo();
            tap.setId(albumPicture.getId());
            //图片的连接地址
            tap.setLink(albumPicture.getLink());
            //图片的alt内容
            tap.setCaption(albumPicture.getCaption());
            //状态,false(0)禁用,true(1)可用
            tap.setStatus(albumPicture.isStatus());
            //是否是封面,false(0)不是,true(1)是
            tap.setCover(albumPicture.isCover());
            //显示的顺序
            tap.setRanking(albumPicture.getRanking());
            //缩略图
            tap.setThumb(albumPicture.getThumb());
            //像册ID
            tap.setAlbumId(albumPicture.getAlbumId());
            return tap;
        };
    }
    public static Function<Album,ThreadsAlbumVo> albumMapper(){
        return album -> {
            ThreadsAlbumVo ta = new ThreadsAlbumVo();
            ta.setId(album.getId());
            ta.setTitle(album.getTitle());
            ta.setVolumes(album.getVolumesId());
            ta.setBoard(album.getBoardId());
            ta.setThreads(album.getTopicId());

            ta.setCoverLink(album.getCoverLink());
            ta.setEntryDateTime(album.getEntryDateTime());
            ta.setMember(album.getMemberId());
            ta.setNickname(album.getMemberNickname());
            ta.setStatus(album.isStatus());
            ta.setThreadsLink(ThreadsVoMapperEntity.produceLink.apply(album.getTopicId(), album.getBoardId(), album.getVolumesId()));
            return ta;
        };
    }
    private ImageIOMeta getImageIOMeta(){
        return new ImageIOMeta(){
            @Override
            public String getImageBucketDomain() {
                return imageBucketDomain;
            }

            @Override
            public String getUploadImageDirectName() {
                return imageBucketUploadDirect;
            }
        };
    }
    public static AnonyActionEventCulpritor getAnonyInstance(final long memberId, final String memberNickname, final String anonyStr, final String ipAddr, final String token) {
        return new AnonyActionEventCulpritor() {
            @Override
            public boolean isAnony() {
                return Commons.isNotBlank(anonyStr) && "1".equals(anonyStr);
            }

            @Override
            public long getMemberId() {
                return memberId;
            }

            @Override
            public String getMemberNickname() {
                return memberNickname;
            }

            @Override
            public String getIpAddr() {
                return ipAddr;
            }

            @Override
            public String getToken() {
                return token;
            }

            @Override
            public String getUserAgent() {
                return "";
            }
        };
    }
    public static ActionEventCulpritor getInstance(final long memberId, final String memberNickname, final String ipAddr, final String token) {
        return new ActionEventCulpritor() {
            @Override
            public long getMemberId() {
                return memberId;
            }

            @Override
            public String getMemberNickname() {
                return memberNickname;
            }

            @Override
            public String getIpAddr() {
                return ipAddr;
            }

            @Override
            public String getToken() {
                return token;
            }

            @Override
            public String getUserAgent() {
                return "";
            }
        };
    }
}
/*
/threads/{id}
/threads/{id}/fold/{board}/{volume}
/threads/{id}/top
/threads/{id}/good
/threads/{id}/lock
/threads/{id}/lock/cancel
/threads/{id}/remove
/threads/{id}/favorite
/threads/{id}/favorite/cancel
/threads/{id}/like
/threads/{id}/like/cancel
/threads/{id}/browse
/threads/{id}/content
/threads/{id}/content/{scale}
/threads/{id}/favorite/member/{member}
/threads/{id}/like/member/{member}
/threads/{id}/report
/threads/{id}/move
/threads/{id}/config
/threads/{id}/stats
/threads/{id}/stats/update
/threads/{id}/cover/{scale}
/threads/volume/{volume}/recent
/threads/volume/{volume}/maxreply
/threads/volume/{volume}/good
/threads/board/{board}/{category}/{page}/{number}
/threads/board/{board}/top
/threads/board/{board}/recent
/threads/board/{board}/recent/{stamp}
/threads/board/{board}/recent/{stamp}/size
/threads/member/recent/{member}/{page}/{number}
/threads/member/reply/{member}/{page}/{number}
/threads/member/reply/{member}
/threads/member/recent/{member}
/threads/member/popular/{member}
/threads/rss/recent/board/{board}
/threads/rss/recent
/threads/stats/maxreply
/threads/category/{board}/{volume}
/threads/home/recent
/threads/feedback
/threads/create
/threads/album/picture/multi
/threads/album/multi
/threads/album/{album}
/threads/album/{album}/pictures
/threads/search/{word}/{page}/{number}
/threads/{id}/relate
/threads/{id}/tag
/threads/{id}/tag/create
/threads/tag/{id}/remove
/threads/tag/hot
/threads/carousel/{volumes}/{board}
/threads/carousel/{title}
/threads/{id}/check/{member}/operate
* */